import type { MutableRefObject } from 'react';
import type { FetchState, Options, Service, PluginReturn } from './index.type';
import { isEmpty } from 'lodash-es';
import { getType } from '../utils';
export default class Fetch<TData = any, TRunArgs extends any[] = any[]> {
  public state: FetchState<TData, TRunArgs> = {
    loading: false,
    data: undefined,
    runArgs: undefined,
    error: undefined,
  };

  private count: number = 0;

  public options: Options<TData, TRunArgs>;
  public serviceRef: MutableRefObject<Service<TData, TRunArgs>>;
  public pluginImpls: PluginReturn<TData, TRunArgs>[] = [];
  public updateUseRequest: () => void;

  constructor(serviceRef: MutableRefObject<Service<TData, TRunArgs>>, updateUseRequest: () => void, options?: Options<TData, TRunArgs>) {
    this.serviceRef = serviceRef;
    this.options = options ?? {};
    this.updateUseRequest = updateUseRequest;
  }

  private runPluginHandler(eventName: keyof PluginReturn, ...rest: any[]) {
    //@ts-ignore
    const resArray = this.pluginImpls.map((pluginObj) => pluginObj[eventName]?.(...rest)).filter((item) => !isEmpty(item));
    // resArray = [{loading:false},{loading:true},{data:123}], ...resArray =  {loading:false} , {loading:true}, {data:123}三个对象
    return Object.assign({}, ...resArray);
  }

  public setState(s: Partial<FetchState<TData, TRunArgs>>) {
    this.state = { ...this.state, ...s };
    this.updateUseRequest(); //强制更新useRequest
  }

  public async runAsync(...runArgs: TRunArgs): Promise<TData> {
    this.count += 1;
    const currentCount = this.count;
    const {
      returnNow = false, //useCachePlugin.onBefore() => ({returnNow:true,data,loading:false})
      ...restState //useLoadingDelayPlugin.onBefore() => ({loading:false})
    } = this.runPluginHandler('onBefore', runArgs);
    this.setState({
      loading: true,
      ...restState,
      runArgs,
    });
    if (returnNow) {
      return Promise.resolve(restState.data as TData);
    }

    this.options.onBefore?.(runArgs);

    try {
      // useCachePlugin.onRequest() => this.serviceRef.current(runArgs)
      let servicePromise = this.runPluginHandler('onRequest', this.serviceRef.current, runArgs);

      if (isEmpty(servicePromise)) {
        servicePromise = this.serviceRef.current(...runArgs);
      }
      const res = await servicePromise;

      if (currentCount !== this.count) {
        // prevent run.then when request is canceled
        return new Promise(() => {});
      }

      this.setState({
        data: res,
        loading: false,
        error: undefined,
      });

      this.options.onSuccess?.(res, runArgs);
      this.runPluginHandler('onSuccess', res, runArgs);

      this.options.onFinally?.(runArgs, res, undefined);
      this.runPluginHandler('onFinally', runArgs, res, undefined);

      return res;
    } catch (error) {
      if (currentCount !== this.count) {
        // prevent run.then when request is canceled
        return new Promise(() => {});
      }
      const stateError = error as Error;
      this.setState({
        error: stateError,
        loading: false,
        data: undefined,
      });
      this.options.onError?.(stateError, runArgs);
      this.runPluginHandler('onError', error, runArgs);

      this.options.onFinally?.(runArgs, undefined, stateError);
      this.runPluginHandler('onFinally', runArgs, undefined, error);
      throw error;
    }
  }

  run(...runArgs: TRunArgs) {
    this.runAsync(...runArgs).catch((error) => {
      if (!this.options.onError) {
        console.error(error);
      }
    });
  }

  public refresh() {
    // @ts-ignore
    this.run(...(this.state.runArgs || []));
  }
  refreshAsync() {
    // @ts-ignore
    return this.runAsync(...(this.state.params || []));
  }

  cancel() {
    this.count += 1;
    this.setState({
      loading: false,
    });

    this.runPluginHandler('onCancel');
  }

  public mutate(data: TData | ((oldData: TData) => TData)) {
    // @ts-ignore
    const targetData = getType(data) === 'function' ? data(this.state.data) : data;
    this.runPluginHandler('onMutate', targetData);
    this.setState({
      data: targetData,
    });
  }
}